﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using NHibernate;
using NHibernate.Proxy;
using NHibernate.Collection;
using System.Collections;

namespace BuildHelper.Domain
{
    public abstract class BaseDomainObject : INotifyPropertyChanged, IDomainSaveObject
    {
        #region CTOR
        protected BaseDomainObject()
        {
            ObjectState = Domain.DomainObjectState.Unknown;
        }
        #endregion

        #region Fields
        private List<PropertyInfo> _ChildProperties = new List<PropertyInfo>();
        private HashSet<string> _ModifiedProperties;
        private Dictionary<string, object> _OrgValues;
        private Dictionary<string, PropertyInfo> _PropertiesMap;
        private DomainObjectState _ObjectState;
        private BaseDomainObject _Parent;
        private int lockEvents = 0;
        private HashSet<BaseDomainObject> ModifiedChildren = new HashSet<BaseDomainObject>();
        #endregion

        #region Properties
        [SkipProperty]
        protected virtual bool AutoInitializeLazy
        {
            get { return true; }
        }
        [SkipProperty]
        protected virtual bool EventsLocked
        {
            get { return lockEvents > 0; }
        }
        [SkipProperty]
        protected virtual HashSet<string> ModifiedProperties
        {
            get
            {
                if (_ModifiedProperties == null)
                    _ModifiedProperties = new HashSet<string>();
                return _ModifiedProperties;
            }
        }
        [SkipProperty]
        protected virtual Dictionary<string, object> OryginalValues
        {
            get
            {
                if (_OrgValues == null)
                    _OrgValues = new Dictionary<string, object>();
                return _OrgValues;
            }
        }
        [SkipProperty]
        protected virtual Dictionary<string, PropertyInfo> PropertiesMap
        {
            get
            {
                if (_PropertiesMap == null)
                {
                    _PropertiesMap = new Dictionary<string, PropertyInfo>();
                    PropertyInfo[] pis = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                    object[] attribs;
                    bool skip;
                    foreach (PropertyInfo pi in pis)
                    {
                        if (!_PropertiesMap.ContainsKey(pi.Name))
                        {
                            skip = false;
                            attribs = pi.GetCustomAttributes(typeof(SkipPropertyAttribute), false);
                            if (attribs != null && attribs.Length > 0)
                                skip = true;
                            if (!skip)
                                _PropertiesMap.Add(pi.Name, pi);
                        }
                    }
                }
                return _PropertiesMap;
            }
        }
        [SkipProperty]
        public virtual object this[string property]
        {
            get
            {
                ValidateProperty(property);
                return PropertiesMap[property].GetValue(this, null);
            }
            set
            {
                ValidateProperty(property);
                PropertiesMap[property].SetValue(this, value, null);
            }
        }
        
        public virtual DomainObjectState ObjectState
        {
            get { return _ObjectState; }
            private set { SetPropertyValue("ObjectState", ref _ObjectState, value); }
        }

        [SkipProperty]
        public virtual BaseDomainObject Parent { get { return _Parent; } }

        #endregion

        #region Events
        public virtual event PropertyChangingHandler PropertyChanging;
        public virtual event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Public Methods
        public virtual void Save(ISession session)
        {
            PrepareChildRecords(session);

            if (ObjectState == DomainObjectState.Added)
            {
                session.Save(this);
            }
            else if (ObjectState == DomainObjectState.Modified ||  HasInnerModifications())
            {
                session.Update(this);
            }
            else if (ObjectState == DomainObjectState.Deleted)
            {
                session.Delete(this);
            }
            //foreach (BaseDomainObject child in ModifiedChildren)
            //{
            //    child.Save(session);
            //}
        }
        public virtual void PrepareChildRecords(ISession session)
        {
            object obj;
            List<object> list = new List<object>();

            foreach (PropertyInfo pi in _ChildProperties)
            {
                obj = pi.GetValue(this, null);
                
                if (IsInitialized(obj))
                {
                    if (obj is IList)
                    {
                        list.Clear();

                        foreach (object o2 in obj as IList)
                        {
                            if (o2 is BaseDomainObject && (o2 as BaseDomainObject).ObjectState == DomainObjectState.Deleted)
                            {
                                list.Add(o2);
                            }
                        }
                        foreach (object o2 in list)
                            (obj as IList).Remove(o2);
                    }
                }
            }
        }
        public virtual void SetAdded()
        {
            ModifiedProperties.Clear();
            OryginalValues.Clear();
            ObjectState = DomainObjectState.Added;
        }
        public virtual void Delete()
        {
            ModifiedProperties.Clear();
            OryginalValues.Clear();
            bool skipParent = false;
            if (_Parent != null && ObjectState == DomainObjectState.Added)
                skipParent = true;
            ObjectState = DomainObjectState.Deleted;
            if (skipParent)
                _Parent.RemoveModifiedChild(this);
        }
        public virtual void AcceptChanges(bool Inherit = true)
        {
            foreach (PropertyInfo pi in PropertiesMap.Values)
            {
                OryginalValues[pi.Name] = pi.GetValue(this, null);
            }
            OryginalValues["ObjectState"] = Domain.DomainObjectState.Unchanged;
            ModifiedProperties.Clear();

            ObjectState = Domain.DomainObjectState.Unchanged;
            if (Inherit)
            {
                object obj;
                foreach (PropertyInfo pi in _ChildProperties)
                {
                    obj = pi.GetValue(this, null);
                    if (IsInitialized(obj))
                    {
                        if (obj is IEnumerable)
                        {
                            foreach (object o2 in obj as IEnumerable)
                            {
                                if (o2 is BaseDomainObject)
                                {
                                    (o2 as BaseDomainObject).AcceptChanges(Inherit);
                                }
                            }
                        }
                        if (obj is BaseDomainObject)
                        {
                            (obj as BaseDomainObject).AcceptChanges(Inherit);
                        }
                    }
                }
            }
        }

        public virtual void RejectChanges()
        {
            foreach (PropertyInfo pi in PropertiesMap.Values)
            {
                if (ValidateOryginalProperty(pi.Name))
                    pi.SetValue(this, OryginalValues[pi.Name], null);
            }
            ObjectState = Domain.DomainObjectState.Unchanged;
            ModifiedProperties.Clear();
        }
        public virtual object GetOryginalValue(string propertyName)
        {
            if (HasOryginalValue(propertyName))
                return OryginalValues[propertyName];
            return null;
        }
        public virtual bool HasOryginalValue(string propertyName)
        {
            return OryginalValues.ContainsKey(propertyName);
        }
        public virtual void LockEvents()
        {
            lockEvents++;
        }
        public virtual void UnLockEvents()
        {
            lockEvents--;
            if (lockEvents < 0)
                lockEvents = 0;
        }
        public virtual void InitializeLazy(ISession session)
        {
            object proxy;
            foreach (PropertyInfo pi in PropertiesMap.Values)
            {
                proxy = pi.GetValue(this, null);
                if (!IsInitialized(pi))
                {
                    session.Lock(this, LockMode.None);
                    NHibernateUtil.Initialize(proxy);
                }
            }
        }

        public virtual void AddChildDomainObjectProperty(string property)
        {
            if (ValidateProperty(property))
            {
                _ChildProperties.Add(PropertiesMap[property]);
            }
        }
        public virtual bool HasInnerModifications()
        {
            if (ObjectState == DomainObjectState.ChildModified)
                return true;
            object obj;
            DomainObjectState st;
            foreach (PropertyInfo pi in _ChildProperties)
            {
                obj = pi.GetValue(this, null);
                if (!IsInitialized(obj))
                    continue;
                if (obj is IEnumerable)
                {
                    foreach (object o2 in obj as IEnumerable)
                    {
                        if (o2 is BaseDomainObject)
                        {
                            st = ((BaseDomainObject)o2).ObjectState;
                            if (st == DomainObjectState.Added ||
                                st == DomainObjectState.Deleted ||
                                st == DomainObjectState.Modified)
                                return true;
                            if (((BaseDomainObject)o2).HasInnerModifications())
                                return true;
                        }
                    }
                }
                if (obj is BaseDomainObject)
                {
                    st = ((BaseDomainObject)obj).ObjectState;
                    if (st == DomainObjectState.Added ||
                        st == DomainObjectState.Deleted ||
                        st == DomainObjectState.Modified)
                        return true;
                    if (((BaseDomainObject)obj).HasInnerModifications())
                        return true;
                }
            }
            return false;
        }
        #endregion

        #region Private Implementation
        protected virtual bool IsInitialized(object proxy)
        {
            Type[] interfaces;
            if (proxy != null)
            {
                interfaces = proxy.GetType().GetInterfaces();

                foreach (Type iface in interfaces)
                {
                    if (iface == typeof(INHibernateProxy) ||
                        iface == typeof(IPersistentCollection))
                    {
                        if (!NHibernateUtil.IsInitialized(proxy))
                        {
                            return false;
                        }
                        break;
                    }
                }
            }
            return true;
        }
        protected virtual void SetParent(BaseDomainObject parent)
        {
            _Parent = parent;
        }

        protected virtual void AddModifiedChild(BaseDomainObject obj)
        {
            ModifiedChildren.Add(obj);
            if (this.ObjectState == DomainObjectState.Unchanged)
            {
                if (ModifiedChildren.Count > 0)
                    this.ObjectState = DomainObjectState.ChildModified;
            }
        }
        protected virtual void RemoveModifiedChild(BaseDomainObject obj)
        {
            if (ModifiedChildren.Contains(obj))
            {
                ModifiedChildren.Remove(obj);
                if (this.ObjectState == DomainObjectState.Unchanged)
                {
                    if (ModifiedChildren.Count > 0)
                        this.ObjectState = DomainObjectState.ChildModified;
                }
                else if (this.ObjectState == DomainObjectState.ChildModified)
                {
                    if (ModifiedChildren.Count == 0)
                        this.ObjectState = DomainObjectState.Unchanged;
                }
            }
        }
        protected virtual void OnPropertyChanged(string property)
        {
            if (property != "ObjectState")
            {
                if (ValidateOryginalProperty(property))
                {
                    object org = OryginalValues[property];
                    if (!org.IsTheSame(PropertiesMap[property].GetValue(this, null)))
                        ModifiedProperties.Add(property);
                    else
                        ModifiedProperties.Remove(property);
                }
                else
                {
                    ModifiedProperties.Add(property);
                }
            }
            if (ObjectState == DomainObjectState.Modified)
            {
                if (ModifiedProperties.Count == 0)
                {
                    if (ModifiedChildren.Count > 0)
                        ObjectState = Domain.DomainObjectState.ChildModified;
                    else
                        ObjectState = Domain.DomainObjectState.Unchanged;
                }
            }
            else if (ObjectState == DomainObjectState.Unchanged)
            {
                if (ModifiedProperties.Count > 0)
                    ObjectState = DomainObjectState.Modified;
                else if (ModifiedChildren.Count > 0)
                    ObjectState = Domain.DomainObjectState.ChildModified;
            }

            if (_Parent != null)
            {
                if (ObjectState != DomainObjectState.Unchanged)
                    _Parent.AddModifiedChild(this);
                else
                    _Parent.RemoveModifiedChild(this);
            }

            if (!EventsLocked && PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
        protected virtual void SetPropertyValue<T>(string property, ref T oldValue, T newValue)
        {
            CustomPropertyChangingEventArgs e = OnProperyChanging(property, oldValue, newValue);
            if (!e.Cancel)
            {
                if (!oldValue.IsTheSame(e.NewValue))
                {
                    oldValue = (T)e.NewValue;
                    OnPropertyChanged(property);
                }
            }
        }

        protected virtual CustomPropertyChangingEventArgs OnProperyChanging(string property, object oldValue, object newValue)
        {
            CustomPropertyChangingEventArgs e = new CustomPropertyChangingEventArgs(property, oldValue, newValue);
            if (!EventsLocked && PropertyChanging != null)
                PropertyChanging(this, e);
            return e;
        }

        protected virtual bool ValidateOryginalProperty(string property)
        {
            return ValidateProperty(property) && OryginalValues.ContainsKey(property);
        }
        protected virtual bool ValidateProperty(string property)
        {
            if (property.IsNotNull() && PropertiesMap.ContainsKey(property))
                return true;
            throw new Exception("Invalid property " + property.AddDoubleQuotes());
        }
        #endregion

        public static IList<T> GetAllRecords<T>() where T : BaseDomainObject
        {
            IList<T> res = null;
            using (ISession session = DomainHelper.SessionFactory.OpenSession())
            {
               res = session.CreateCriteria<T>().List<T>();
               if (res != null)
               {
                   foreach (T r in res)
                   {
                       if (r.AutoInitializeLazy)
                           r.InitializeLazy(session);
                   }
               }
            }
            if (res != null)
            {
                foreach (T r in res)
                    r.AcceptChanges();
            }
            
            return res;
        }

        public static T GetById<T>(object id) where T : BaseDomainObject
        {
            using (ISession session = DomainHelper.SessionFactory.OpenSession())
            {
                T res = session.Get<T>(id);
                if (res != null)
                {
                    if (res.AutoInitializeLazy)
                        res.InitializeLazy(session);
                    res.AcceptChanges();
                }
                return res;
            }
        }
    }
    
    public delegate void PropertyChangingHandler(object sender, CustomPropertyChangingEventArgs e);
    
    public class CustomPropertyChangingEventArgs : PropertyChangingEventArgs
    {
        public CustomPropertyChangingEventArgs(string property, object oldValue, object newValue):base(property)
        {
            OldValue = oldValue;
            NewValue = newValue;
        }
        public object OldValue
        {
            get;
            private set;
        }
        public object NewValue
        {
            get;
            set;
        }
        public bool Cancel
        {
            get;
            set;
        }
    }
    
    public enum DomainObjectState
    {
        Unknown,
        Added,
        Modified,
        ChildModified,
        Unchanged,
        Deleted
    }

    public static class DomainHelper
    {
        static ISessionFactory _SessionFactory;

        public static ISessionFactory SessionFactory
        {
            get { return _SessionFactory; }
            set { _SessionFactory = value; }
        }

        public static Exception LastException
        {
            get;
            private set;
        }

        public static bool TrySave(this IDomainSaveObject obj, ISession session)
        {
            LastException = null;
            bool result = true;
            try
            {
                obj.Save(session);
                result = true;
            }
            catch (Exception ex)
            {
                LastException = ex;
                result = false;
            }
            return result;
        }
        public static bool TryCommit(this ITransaction transaction)
        {
            LastException = null;
            bool result = true;
            try
            {
                transaction.Commit();
                result = true;
            }
            catch (Exception ex)
            {
                LastException = ex;
                result = false;
            }
            return result;
        }
        
    }

    [AttributeUsage(AttributeTargets.Property)]
    public sealed class SkipPropertyAttribute : Attribute
    {

    }
}
